간단한 연산자 구현
#include <iostream>
#include <initializer_list>
template <typename T>
class vector{
public:
explicit vector(int size): my_size(size), data(new T[my_size]) {}
const T& operator[](int i) const { check_index(i); return data[i]; }
T& operator[](int i){ check_index(i); return data[i]; }
}
private:
int my_size;
T* data;
};
template <typename T>
inline vector<T> operator+(const vector<T>& x, const vector<T>& y){
x.check_size(size(y));
vector<T> sum(size(x));
for(int i=0; i<size(x); ++i){
sum[i]=x[i]+y[i];
}
return sum;
}
int main(void){
vector<float> x={1.0, 1.0, 2.0, -3.0}, y={1.7, 1.7, 4.0, -6.0}, z={4.1, 4.1, 2.6, 11.0}, w(4);
std::cout<<"x = "<<x<<std::endl;
std::cout<<"y = "<<y<<std::endl;
std::cout<<"z = "<<z<<std::endl;
w=x+y+z;
std::cout<<"w = x + y+ z = "<<w<<std::endl;
return 0;
}
위와 같이 최적화하지 않고 코드를 구성할 경우, 내부에서 임시 변수를 많이 생성한다.
1. x와 y를 더하기 위해 임시 변수 sum 생성(21)
2. x와 y를 읽는 반복문 수행, 요소별로 더한 결과를 sum에 저장
3. return 문에서 sum을 임시 변수, ex t_xy에 복사(25)
4. scope에서 벗어난 후, 소멸자로 sum을 제거
5. t_xy와 z를 더하기 위해 임시 변수 sum 생성
6. t_xy와 z를 읽는 반복문 수행, 요소별 더한 결과 sum에 저장
7. return 문에서 다시 임시 변수 t_xyz에 저장
8. scope에서 벗어난 후, 소멸자로 sum을 제거
9. 연산 이후, t_xy 삭제
10. t_xyz를 읽고, w에 저장
11. t_xyz를 삭제(할당 이후)
C++에서 리턴값이 있을 때, 메모리에 타입에 대한 임시변수를 생성한다.(리턴값이 없는 경우, 생성하지 않음)
최신 컴파일러는 정적 코드 분석과 반환 값 최적화(return value optimization)을 통해 최적화를 수행한다.
임시변수 t_xy 및 t_xyz로의 복사를 방지
1. x와 y를 더하기 위해 임시 변수 sum을 생성(sum_xy)
2. x와 y를 읽는 반복문을 수행, 요소별로 더한 다음 결과를 sum에 저장
3. sum_xy와 z를 더하기 위해 임시 변수 sum을 생성(sum_xyz)
4. sum_xy와 z를 읽는 반복문을 수행, 요소별로 더한 결과를 sum에 저장
5. sum_xy를 삭제
6. sum_xyz를 읽고, w에 저장
7. sum_xyz를 삭제(할당 이후)
최적화를 위해 단일 반복문이나 인라인 함수를 작성
template <typename T>
void inline add3(const vector<T>& x, const vector<T>& y, const vector<T>& z, vector<T>& sum){
x.check_size(size(y));
x.check_size(size(z));
x.check_size(size(sum));
for(int i=0; i<size(x); ++i){
sum[i]=x[i]+y[i]+z[i];
}
}
위와 같이 3개의 벡터에 대한 함수를 호출하는 방식은
할당, 할당 해제 작업이 줄어들고, 읽기와 쓰기 연산을 줄일 수 있지만,
연산자 표기법(operator+)보다 오류가 발생하기 쉬우며, 이식성이 낮다.
고성능 소프트웨어에서 프로그래머가 모든 중요한 작업을 하드 코딩(hard-coded)된 버전으로 구현한다.
길이가 짧은 벡터의 경우, 데이터가 L1 or L2 캐시에 상주할 수 있어서 데이터 전송이 중요하지 않다.
하지만, 할당 및 할당 해제가 심각한 속도 저하의 요인이 된다.
최신 컴퓨터에서는 고정 또는 부동소수점 연산을 실행할 때보다
많은 양의 데이터를 메모리에 읽거나 쓸 때, 훨씬 많은 시간이 소요된다.
메모리 할당, 해제, 쓰기, 읽기를 줄이는 방향으로 코딩을 할 수록 속도가 빠름
표현식 템플릿 클래스(Expression Tempalate, ET)표현식 템플릿은 임시 연산자로 인한 오버헤드를 발생시키지 않고,
원래의 연산자 표기법을 유지하는 것을 목적으로 한다.
벡터를 가리키는 레퍼런스를 유지하면서, 한번 훑을 때 한번에 계산할 수 있게 해주는
중간 개체 클래스를 도입하는 방법
(덧셈 연산은 더 이상 벡터를 반환하지 않고 인수를 참조하는 개체를 반환)
template <typename T>
class vector_sum{
public:
vector_sum(const vector<T>& v1, const vector<T>& v2): v1(v1), v2(v2) {}
friend int size(const vector_sum& x){ return size(x.v1); }
T operator[](int i) const { return v1[i]+v2[i]; }
private:
const vector<T> &v1, &v2;
};
template <typename T>
vector_sum<T> operator+(const vector<T>& x, const vector<T>& y){
return {x, y};
}
vector& operator=(const vector_sum<T>& that){
check_size(size(that));
for(int i=0; i<my_size; ++i) data[i]=that[i];
return *this;
}
vector_sum은 첨자 연산자(operator[])을 이용해서 i번째 항목에 접근하면, 즉석으로 합을 계산한다.
할당 연산자(operator=)를 이용해서 vector& + vector_sum<T>&의 연산을 정의해 주었다.
vector_sum<T>의 대입 연산자 that[i]는 요소별 합, x[i]+y[i]를 계산한다.
- 오직 하나의 반복문만 갖는다.
- 임시 벡터가 없다.
- 추가 메모리 할당 및 할당 해제가 없다.
- 추가 데이터 읽기 및 쓰기 연산이 없다.
vector_sum 개체를 만드는데 드는 비용은 무시할 수 있음
개체는 스택에 보관되며, 메모리 할당을 요구하지 않는다.
중간에 임시 변수를 생성하지 않기 위해서, 클래스로 벡터의 레퍼런스를 보관하고 있다가,
할당 연산을 이용해서 최종적으로 값을 w에 할당할 때, 한번만 반복문을 수행하며,
이 때, vector_sum에 저장하고 있던 레퍼런스에 대하여서 v1[i]+v2[i]를 임시 변수에 저장해서 반환하고,
이를 할당 연산자를 이용해서 w에 개체를 반환한다.
vector 3개의 합을 위한 vector_sum3
template <typename T>
class vector_sum3{
public:
vector_sum3(const vector<T>& v1, const vector<T>& v2, const vector<T>& v3): v1(v1), v2(v2), v3(v3) { }
T operator[](int i) const { return v1[i]+v2[i]+v3[i]; }
private:
const vector<T> &v1, &v2, &v3;
};
template <typename T>
vector_sum3<T> inline operator+(const vecor_sum<T>& x, const vector<T>& y){
return {x.v1, x.v2, y};
}
위와 같이 정의하면, 할당 연산과 덧셈 연산을 추가로 구현해야 하며,
w=x+(y+x)와 같이 vector<T>+vector_sum<T>에 대한 operator+를 따로 구현해 주어야 한다.
제네릭 표현식 템플릿임의의 벡터 타입과 뷰(제네릭)
template <typename V1, typename V2>
inline vector_sum<V1, V2> operator+(const V1& x, const V2& y){
return {x, y};
}
template <typename V1, typename V2>
class vector_sum{
public:
using value_type=std::common_type_t<typename T1::value_type, typename T2::value_type>;
vector_sum(const V1& v1, const V2& v2): v1(v1), v2(v2) {}
value_type operator[](int i) const { return v1[i]+v2[i]; }
private:
const V1& v1;
const V2& v2;
};
만일 vector_sum 클래스가 value_type을 명시적 선언을 필요로 하지 않는다면,
decltype(auto)를 이용해서 리턴 타입으로 사용할 수 있다.(컴파일러가 타입을 전적으로 추론함)
vector 클래스 대입 연산자 일반화
template <typename T>
class vector{
public:
template <typename Src>
vector& operator=(const Src& that){
check_size(size(that));
for(int i=0; i<my_size; ++i) data[i]=that[i];
return *this;
}
};
위의 대입 연산자는 전용 복사 할당 연산자가 필요한 vector<T>를 제외한 모든 타입을 허용한다.
연산자 오버로드를 이용하면, 코드를 깔끔하게 작성할 수 있다.
하지만, 연산자 오버로드를 이용한 연산은 비용이 너무 비싸다.
(임시 변수 생성 및 벡터 및 행렬 개체를 복사하는 오버로드)
제네릭과 표현식 템플릿의 도입으로 이를 해결할 수 있다.